package edu.utexas.esel.jgraphpad.archpadplugin;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.Properties;

import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.JTabbedPane;
import javax.swing.filechooser.FileFilter;
import javax.xml.parsers.ParserConfigurationException;

import org.jgraph.graph.AttributeMap;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.GraphCell;
import org.jgraph.graph.GraphConstants;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.jgraph.JGraphEditor;
import com.jgraph.editor.JGraphEditorFactory;
import com.jgraph.editor.JGraphEditorFile;
import com.jgraph.editor.JGraphEditorKit;
import com.jgraph.editor.JGraphEditorModel;
import com.jgraph.editor.JGraphEditorPlugin;
import com.jgraph.editor.JGraphEditorResources;
import com.jgraph.editor.JGraphEditorSettings;
import com.jgraph.editor.JGraphEditorTool;
import com.jgraph.editor.JGraphEditorFactory.FactoryListener;
import com.jgraph.editor.JGraphEditorTool.UpdateListener;
import com.jgraph.editor.factory.JGraphEditorFactoryMethod;
import com.jgraph.editor.persistence.JGraphPersistenceManager;
import com.jgraph.editor.persistence.JGraphPersistenceManagerFactory;
import com.jgraph.editor.persistence.JGraphPersistenceManagerFactoryMethod;
import com.jgraph.pad.dialog.JGraphpadDialogs;
import com.jgraph.pad.factory.JGraphpadPane.LeftTabFactoryMethod;
import com.jgraph.pad.graph.JGraphpadGraphConstants;
import com.jgraph.pad.graph.JGraphpadVertexRenderer;
import com.jgraph.pad.tool.JGraphpadVertexTool;
import com.jgraph.pad.util.JGraphpadFileFilter;
import com.jgraph.pad.util.JGraphpadFocusManager;
import com.jgraph.pad.util.JGraphpadImageIcon;

import edu.utexas.esel.jgraphpad.archpadplugin.action.ArchPadCellAction;
import edu.utexas.esel.jgraphpad.archpadplugin.action.ArchPadFileAction;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ElementGraphCell;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element;
import edu.utexas.esel.jgraphpad.archpadplugin.model.ElementTreeNode;
import edu.utexas.esel.jgraphpad.archpadplugin.model.RegionManager;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element.ElementDeclaration;
import edu.utexas.esel.jgraphpad.archpadplugin.persistence.ArchPadXMLPersistenceManager;
import edu.utexas.esel.jgraphpad.archpadplugin.tools.ArchPadEdgeTool;
import edu.utexas.esel.jgraphpad.archpadplugin.ui.RegionTree;

/**
 * This is the main class of the ArchPad plugin that enables building,
 * and analysis of software architecture models. The software architecture
 * model is specified by an XML XSD schema.The implementation of the plugin
 * make only assumption for the existence of the "document", "model",
 * "component", "connector" elements. It also assumes that each "component"
 * or "connector" had an ID attribute as well as each element in the
 * document hierarchy has an attribute "rationale". Another assumption is
 * that each element type in the schema is declared at the top level. Aside
 * from this assumptions users can change the XSD schema thus changing the
 * model. The plug in so configured via properties files as follows:
 * <p>
 * actions.properties - Registers action performed by the plugin.
 * archpad.properties - Defines plugin properties (for example the XDS schema)
 * icons.properties - Defines the mapping from strings to icons.
 * string.properties - Defines the strings for internationalization.
 * tools.properties - Registers tools provided by the plugin.
 * ui.xml - Defines the UI of the plugin.
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class ArchPadPlugin implements JGraphEditorPlugin {

	/**
	 * Defines the path to the software architecture model configuration file.
	 * This should also work with URLs (untested).
	 */
	public static final String PATH_ARCHPAD_PROPERTIES =
		"/edu/utexas/esel/jgraphpad/archpadplugin/resources/archpad.properties";

	/**
	 * Defines the name for the properties file software architecture model.
	 */
	public static final String NAME_ARCHPAD_PROPERTIES = "archPadProperties";

	/**
	 * Defines the path to the UI configuration to be merged into the existing
	 * UI configuration.
	 */
	public static final String PATH_UICONFIG =
		"/edu/utexas/esel/jgraphpad/archpadplugin/resources/ui.xml";

	/**
	 * Defines the name for the UI XML document in the editor settings.
	 */
	public static String NAME_UICONFIG = "ui";

	/**
	 * Defines the name for the componentTool.
	 */
	public static final String NAME_COMPONENT_TOOL = "componentTool";

	/**
	 * Defines the name for the connectorTool.
	 */
	public static final String NAME_CONNECTOR_TOOL = "connectorTool";

	/**
	 * Defines the name for the connectionTool.
	 */
	public static final String NAME_CONNECTION_TOOL = "connectionTool";

	/**
	 * The default ArchPad cell shape.
	 */
	public static final boolean DEFAULT_VERTEX_SIZABILITY = false;

	/**
	 * The default ArchPad cell shape.
	 */
	public static final int DEFAULT_VERTEX_SHAPE =
		JGraphpadVertexRenderer.SHAPE_RECTANGLE;

	/**
	 * The size of the side of an ArchPad square cell.
	 */
	public static final float DEFAULT_VERTEX_SIDE = 65;

	/**
	 * The default font for ArchPad cells.
	 */
	public static final Font DEFAULT_VERTEX_FONT =
		new Font("Times New Roman", Font.PLAIN, 10);

	/**
	 * Defines the {@link ElementDeclaration} of the "component" element.
	 */
	public static final ElementDeclaration TYPE_DECLARATION_COMPONENT =
		new ElementDeclaration("component", "component", 1, 1);

	/**
	 * Defines the {@link ElementDeclaration} of the "component" element.
	 */
	public static final ElementDeclaration TYPE_DECLARATION_REGION =
		new ElementDeclaration("region", "region", 1, 1);

	/**
	 * Defines the {@link ElementDeclaration} of the "composition" element.
	 */
	public static final ElementDeclaration TYPE_DECLARATION_COMPOSITION =
		new ElementDeclaration("composition", "composition", 1, 1);

	/**
	 * Defines the {@link ElementDeclaration} of the "connector" element.
	 */
	public static final ElementDeclaration TYPE_DECLARATION_CONNECTOR =
		new ElementDeclaration("connector", "connector", 1, 1);

	private static final String FILE = "File";

	/**
	 * Key for the icon show in cells created  by the component tool.
	 */
	public static final String KEY_COMPONENT_TOOL_CELL_ICON =
		"componentTool.cell.icon";

	/**
	 * Key for the icon show in cells created  by the connector tool.
	 */
	public static final String KEY_CONNECTOR_TOOL_CELL_ICON =
		"connectorTool.cell.icon";

	private static final String KEY_COMPOSITION_ICON =
		"composeComposition.cell.icon";

	/**
	 * Defines the key used to identify the composition prototype.
	 */
	public static String KEY_COMPOSITION_PROTOTYPE =
		"compositionPrototype";

	private UpdateListener mUpdateListener = new UpdateListener() {
			public void onUpdate(JGraphEditorTool tool) {
				boolean enabled = Utils.isCurrentArchPadDiagram();
				tool.setEnabled(enabled);
			}
		};

	public void initialize(JGraphEditor editor, Node configuration)
			throws ParserConfigurationException, SAXException, IOException {
		load(editor);
        configure(editor);
        add(editor);
        registerFactoryMethods(editor.getFactory());
        registerFactoryListeners(editor.getFactory());
	}

	/**
	 * Registers {@link JGraphEditorFactoryMethod}s.
	 *
	 * @param factory The {@link JGraphEditorFactory}.
	 */
	private void registerFactoryMethods(JGraphEditorFactory factory) {
		factory.addMethod(new RegionTree.FactoryMethod(
				RegionTree.FactoryMethod.NAME));
	}

	/**
	 * Registers {@link FactoryListener}s.
	 *
	 * @param factory The {@link JGraphEditorFactory}.
	 */
	private void registerFactoryListeners(final JGraphEditorFactory factory) {
		factory.addFactoryListener(LeftTabFactoryMethod.NAME,
        		new FactoryListener() {
			@Override
			public void onExecute(Component component) {
				JTabbedPane tabbedPane = (JTabbedPane) component;
				Component tab = factory.executeMethod(
						RegionTree.FactoryMethod.NAME);
				String title = JGraphEditorResources.getString(
						RegionTree.KEY_REGIONS_TAB_NAME);
				tabbedPane.addTab(title, tab);
			}
        });
	}

	/**
	 * Loads all settings, bundles, properties, etc in the {@link JGraphEditor}.
	 *
	 * @param editor The editor.
	 * @throws IOException If an error occurs while reading some of the files.
	 * @throws ParserConfigurationException If an error occurs while configuring
	 *         an XML SAX parser.
	 * @throws SAXException If an error occurs while parsing
	 *         an XML.
	 */
	private void load(JGraphEditor editor) throws IOException,
	        ParserConfigurationException, SAXException {
		loadSettings(editor.getSettings());
		addResourceBundles();
	}

	/**
	 * Configures all {@link JGraphEditor} features that need to be configured.
	 *
	 * @param editor The editor.
	 */
	private void configure(JGraphEditor editor) {
		setTransientPropeties();
        configurePersistence(editor.getSettings());
        configureArchPadElementModel(editor.getSettings());
        configureRegionManager(editor.getKit());
	}

	/**
	 * Adds to the {@link JGraphEditor} all tools, actions, etc.
	 *
	 * @param editor The editor.
	 */
	private void add(JGraphEditor editor) {
		addTools(editor);
        addActions(editor);
        addToolsAndActionsManager(editor);
	}

	/**
	 * Loads all files that must be added to {@link JGraphEditorSettings}.
	 *
	 * @param settings The settings.
	 * @throws SAXException If an error occurs while paring an XML file.
	 * @throws ParserConfigurationException If an error occurs while
	 *         configuring the SAX XML parser.
	 * @throws IOException If an error occurs while reading a file.
	 */
	private void loadSettings(JGraphEditorSettings settings)
	        throws SAXException, ParserConfigurationException, IOException {
		settings.add(NAME_ARCHPAD_PROPERTIES,
				JGraphEditorResources.getInputStream(PATH_ARCHPAD_PROPERTIES));

		settings.add(NAME_UICONFIG, JGraphEditorSettings
				.parse(JGraphEditorResources.getInputStream(PATH_UICONFIG)));
	}

	/**
	 * Adds all resource bundles.
	 */
	private void addResourceBundles() {
		JGraphEditorResources.addBundles(new String[] {
		    "edu.utexas.esel.jgraphpad.archpadplugin.resources.strings",
			"edu.utexas.esel.jgraphpad.archpadplugin.resources.tools",
			"edu.utexas.esel.jgraphpad.archpadplugin.resources.actions",
			"edu.utexas.esel.jgraphpad.archpadplugin.resources.icons"});
	}

	/**
	 * Sets as transient all properties that should not be persisted.
	 */
	private void setTransientPropeties() {
		JGraphEditorModel.makeTransient(DefaultGraphCell.class, "userObject");
		JGraphEditorModel.makeTransient(DefaultEdge.class, "userObject");
		JGraphEditorModel.makeTransient(DefaultPort.class, "userObject");
	}

	/**
	 * Configures the persistence by adding a {@link JGraphPersistenceManager}
	 * for handling ArchPad files.
	 *
	 * @param settings The {@link JGraphEditorSettings} of the application.
	 */
	private void configurePersistence(JGraphEditorSettings settings) {
        Properties properties = settings.getProperties(NAME_ARCHPAD_PROPERTIES);
        JGraphPersistenceManagerFactoryMethod<JGraphEditorFile> factoryMethod =
        	new ArchPadXMLPersistenceManager.FactoryMethod(properties);

        registerPersistenceManagerFactoryMethod(factoryMethod);
        configureFileChoosers(factoryMethod.getFileExtensions());
	}

	/**
	 * Registers the persistence manager factory method.
	 *
	 * @param method The factory method.
	 */
	private void registerPersistenceManagerFactoryMethod(
			JGraphPersistenceManagerFactoryMethod<JGraphEditorFile> method) {
		JGraphPersistenceManagerFactory.getInstance()
        .addFactoryMethod(method);
	}

	/**
	 * Configure file choosers to handle ArchPad file <code>extensions</code>.
	 *
	 * @param extensions The file extensions.
	 */
	private void configureFileChoosers(String [] extensions) {
		JGraphpadDialogs dialogs = JGraphpadDialogs.getSharedInstance();
		JFileChooser openFileChooser = dialogs.getOpenFileChooser();
		JFileChooser saveFileChooser = dialogs.getSaveFileChooser();
		StringBuilder description = new StringBuilder();

		for (String extension : extensions) {
			description.delete(0, description.length());
			description.append(extension.toUpperCase());
			description.append(" ");
			description.append(JGraphEditorResources.getString(FILE));
			description.append(" (.");
			description.append(extension);
			description.append(")");

			FileFilter filter = new JGraphpadFileFilter("." + extension,
					description.toString());

			openFileChooser.addChoosableFileFilter(filter);
			saveFileChooser.addChoosableFileFilter(filter);
		}
	}

	/**
	 * Adds the ArchPad plugin tools to the {@link JGraphEditor}.
	 *
	 * @param editor The editor.
	 */
	private void addTools(JGraphEditor editor) {
		JGraphEditorKit kit = editor.getKit();

		JGraphEditorTool componentTool = createVertexTool(kit,
			NAME_COMPONENT_TOOL, TYPE_DECLARATION_COMPONENT,
			KEY_COMPONENT_TOOL_CELL_ICON);
		kit.addTool(componentTool);

		JGraphEditorTool connectorTool = createVertexTool(kit,
			NAME_CONNECTOR_TOOL, TYPE_DECLARATION_CONNECTOR,
			KEY_CONNECTOR_TOOL_CELL_ICON);
			kit.addTool(connectorTool);

		kit.addTool(createEdgeTool(kit));
	}

	/**
	 * Creates an element tool.
	 *
	 * @param kit The {@link JGraphEditorKit}.
	 * @return The {@link JGraphEditorTool}.
	 */
	private JGraphEditorTool createVertexTool(JGraphEditorKit kit,
			String name, ElementDeclaration type, String iconPathKey) {

		Element model = createArchPadElementModel(type);
		AttributeMap attributes = kit.createAttributeMap();
		ElementGraphCell vertex = new ElementGraphCell(model, attributes);
		kit.configureVertex(vertex);
		customizeCell(vertex, iconPathKey, type.getName());

		JGraphpadVertexTool tool = new JGraphpadVertexTool(name, vertex) {
			@Override
			protected void processMouseDraggedEvent(MouseEvent event) {
				/* do not support */
			}
		};
		tool.setThreshold(0);
		tool.setSingleClickSize(new Dimension((int) DEFAULT_VERTEX_SIDE,
			(int) DEFAULT_VERTEX_SIDE));
      	tool.setUpdateListener(mUpdateListener);

		return tool;
	}

	/**
	 * Creates the connectionTool.
	 *
	 * @param kit The {@link JGraphEditorKit}.
	 * @return An {@link ArchpadEdgeTool}.
	 */
	private JGraphEditorTool createEdgeTool(JGraphEditorKit kit) {
		DefaultEdge edge = kit.createEdge(null);

		AttributeMap attributes = edge.getAttributes();
		GraphConstants.setLineBegin(attributes, GraphConstants.ARROW_NONE);
		GraphConstants.setLineEnd(attributes, GraphConstants.ARROW_NONE);
		GraphConstants.setRouting(attributes, GraphConstants.ROUTING_SIMPLE);

		JGraphEditorTool tool = new ArchPadEdgeTool(NAME_CONNECTION_TOOL, edge);
		tool.setUpdateListener(mUpdateListener);

		return tool;
	}

	/**
	 * Adds ArchPad actions to the {@link JGraphEditor}.
	 *
	 * @param editor The editor.
	 */
	private void addActions(JGraphEditor editor) {
		JGraphEditorKit kit = editor.getKit();

		GraphCell prototype = createCompositionPrototype(kit);
		editor.getSettings().putObject(KEY_COMPOSITION_PROTOTYPE, prototype);

		kit.addBundle(new ArchPadFileAction.AllActions(editor));
		kit.addBundle(new ArchPadCellAction.AllActions(editor));
	}

	/**
	 * Sets the prototype used for creating a composition group.
	 */
	private GraphCell createCompositionPrototype(JGraphEditorKit kit) {
		Element model = createArchPadElementModel(
			TYPE_DECLARATION_COMPOSITION);
		AttributeMap attributes = kit.createAttributeMap();
		ElementGraphCell prototype = new ElementGraphCell(model, attributes);
		kit.configureVertex(prototype);
		customizeCell(prototype, KEY_COMPOSITION_ICON,
			TYPE_DECLARATION_COMPOSITION.getName());

		return prototype;	
	}

	/**
	 * Customizes a <code>cell</code> in ArchPad specific manner.
	 *
	 * @param cell The cell.
	 * @param iconPathKey The key to lookup the {@link Icon} for the cell.
	 */
	@SuppressWarnings("unchecked")
	// JGraph uses raw types
	private void customizeCell(GraphCell cell, String iconPathKey,
			String type) {
		AttributeMap attributes = cell.getAttributes();
		GraphConstants.setFont(attributes, DEFAULT_VERTEX_FONT);        
        GraphConstants.setSizeable(attributes, DEFAULT_VERTEX_SIZABILITY);
		JGraphpadGraphConstants.setVertexShape(attributes,
			DEFAULT_VERTEX_SHAPE);
		String iconPath = JGraphEditorResources.getString(iconPathKey);
		JGraphpadImageIcon icon = new JGraphpadImageIcon(iconPath);
		GraphConstants.setIcon(attributes, icon);
		attributes.put(ElementGraphCell.ATTRIBUTE_ELEMENT_TYPE, type);
	}

	/**
	 * Creates an {@link Element} for a given
	 * {@link ElementDeclaration}.
	 *
	 * @param declaration The declaration.
	 * @return The model.
	 */
	private Element createArchPadElementModel(
			ElementDeclaration declaration) {
		ElementTreeNode root = new ElementTreeNode(declaration);
		Element element = new Element(root);
		element.addRequiredDescendents(root);
		return element;
	}

	/**
	 * Configures the {@link Element} used by ArchPad.
	 *
	 * @param settings The application {@link JGraphEditorSettings}. 
	 */
	private void configureArchPadElementModel(JGraphEditorSettings settings) {
		Properties properties = settings.getProperties(NAME_ARCHPAD_PROPERTIES);
		Element.configureArchPadElementModel(properties);
	}

	/**
	 * Configures the {@link RegionManager} by setting its vertex and edge
	 * prototypes.
	 */
	private void configureRegionManager(JGraphEditorKit kit) {
	    RegionManager.setRegionVertexPrototype(
	    	createRegionManagerVertexPrototype(kit));
	    RegionManager.setRegoinEdgePrototype(
	    	createRegionManagerEdgePrototype());
	}

	/**
	 * Creates a {@link RegionManager} vertex prototype.
	 *
	 * @return The prototype.
	 */
	private ElementGraphCell createRegionManagerVertexPrototype(
			JGraphEditorKit kit) {
		Element element = createArchPadElementModel(
			ArchPadPlugin.TYPE_DECLARATION_REGION);
		ElementGraphCell vertex = new ElementGraphCell(element,
			kit.createAttributeMap());
		return vertex;
	}

	/**
	 * Creates a {@link RegionManager} edge prototype.
	 *
	 * @return The prototype.
	 */
	private DefaultEdge createRegionManagerEdgePrototype() {
		return new DefaultEdge();
	}

	/**
	 * Registers a {@link ToolsAndActionsManager} manager.
	 *
	 * @param manager The manager.
	 */
	private void addToolsAndActionsManager(JGraphEditor editor) {
		final ToolsAndActionsManager finalManager =
			new ToolsAndActionsManager(editor);
		JGraphpadFocusManager.getCurrentGraphFocusManager()
		    .addPropertyChangeListener(new PropertyChangeListener() {
		    	public void propertyChange(PropertyChangeEvent e) {
				    finalManager.update();
			    }
		    });
	}
}
